import fse from 'fs-extra';
import handlebars from 'handlebars';
import path from 'path';

import { commonUtil } from '../../common-util';

/**
 * 模板文件路径
 *
 * @type {string}
 */
const BASE_SOURCE: string = path.join(__filename, '../', '../', '../', '../', '../', 'resources');

/**
 * 节点接口的抽象子类，定义了添加和获取字节点的方法
 */
export class Node<T extends Node<any>> {

  /**
   * 节点名称
   */
  name: string;

  /**
   * 子节点
   */
  children: { [key: string]: T } = {};

  /**
   * 节点类构造函数
   *
   * @param {string} name 节点名
   * @protected
   */
  protected constructor(name: string) {
    this.name = name;
  }

  /**
   * 添加子节点
   *
   * @param {T} child
   */
  add(child: T): this {
    this.children[child.name] = child;
    return this;
  }

  /**
   * 通过节点名称获取子节点
   *
   * @param {string} name
   * @return {T}
   */
  node(name: string): T {
    return this.children[name];
  }
}

export type DataNode<T extends DataNode<any>> = {
  data: object,
  node(name: string): T
}

/**
 * 生成文件对象接口
 */
interface IFile {

  /**
   * 节点名
   * @type {string}
   */
  name: string;

  /**
   * 文件路径
   */
  path: string;

  /**
   * 填充数据
   */
  data?: object;

  /**
   * 更新文件内容
   *
   * @return {Promise<void>}
   */
  refresh(): Promise<void>;
}


/**
 * 文件节点类
 *
 * 继承了默认的节点方法
 */
export abstract class AbsFileNode<AbsFileNode> extends Node<any> implements IFile {

  /**
   * 节点名
   * @type {string}
   */
  name: string;

  /**
   * 子节点
   * @type {{[p: string]: AbsFileNode}}
   */
  children: { [p: string]: AbsFileNode } = {};

  /**
   * 文件路径
   */
  path: string;

  /**
   * 文件填充信息
   */
  data?: object;

  /**
   * 旧的填充数据的hash值
   *
   * @type {string | undefined}
   * @private
   */
  protected _oldHash: string | undefined;

  /**
   *  文件节点对象类构造方法
   *
   * @param {string} name 节点名
   * @param {string} path 对象路径
   * @param {object} data 文件填充对象
   */
  protected constructor(name: string, path: string, data?: object) {
    super(name);
    this.name = name;
    this.path = path;
    this.data = data;
  }

  /**
   * 更新文件内容
   *
   * @return {Promise<void>}
   */
  public abstract refresh(): Promise<void> ;
}

/**
 * 模板生成之后对应文件夹的节点对象
 */
export class DirNode<AbsFileNode> extends AbsFileNode<AbsFileNode> {

  /**
   *  文件夹节点对象类构造方法
   *
   * @param {string} name 节点名
   * @param {string} path 对象路径
   */
  public constructor(name: string, path: string) {
    super(name, path, undefined);
  }

  /**
   * 更新文件内容
   *
   * @return {Promise<void>}
   */
  public async refresh(): Promise<void> {
    // 遍历子节点执行刷新
    for (const childNode of Object.values(this.children)) {
      await (childNode as unknown as IFile).refresh();
    }
  }
}

/**
 * 复制的文件节点对象
 */
export class CopyNode<DirNode> extends AbsFileNode<DirNode> {

  /**
   *  文件节点对象类构造方法
   *
   * @param {string} name 节点名
   * @param {string} path 对象路径
   * @param {object} data 文件填充对象
   */
  public constructor(name: string, path: string, data?: object) {
    super(name, path, data);
  }

  /**
   * @deprecated 复制的文件节点对象不需要刷新
   *
   * @return {Promise<void>}
   */
  public async refresh(): Promise<void> {
    return Promise.resolve();
  }
}

/**
 * 文件节点对象
 */
export class RenderNode<DirNode> extends AbsFileNode<DirNode> {

  /**
   * 文件内容渲染函数
   *
   * @type {HandlebarsTemplateDelegate}
   * @private
   */
  private readonly _tempFun: HandlebarsTemplateDelegate;

  /**
   *  文件节点对象类构造方法
   *
   * @param {string} name 节点名
   * @param {string} path 对象路径
   * @param {object} data 文件填充对象
   * @param tempFun 将文件填充对象渲染成文件内容的对象
   */
  public constructor(name: string, path: string, data: object, tempFun: HandlebarsTemplateDelegate) {
    super(name, path, data);
    this._tempFun = tempFun;
    this._oldHash = commonUtil.hash(this.data);
  }

  /**
   * 更新文件内容，文件对象刷新方法为将填充内容渲染后写入
   *
   * @return {Promise<void>}
   */
  public async refresh(): Promise<void> {
    // 填充对象无变化直接跳过
    if (this._oldHash === commonUtil.hash(this.data)) {
      return Promise.resolve();
    }
    const text = this._tempFun(this.data);
    await fse.writeFile(this.path, text);

    // 更新填充对象hash值
    this._oldHash = commonUtil.hash(this.data);
  }
}

/**
 * 模板文件接口
 *
 */
interface Template {

  /**
   * 源文件路径
   */
  source?: string;

  /**
   * 填充数据
   */
  data?: object;

  /**
   * 生成文件的函数
   *
   * @param {string} target 生成路径
   * @return {Promise<File>}
   */
  generated: (target: string) => Promise<AbsFileNode<AbsFileNode<any>>>;
}

/**
 * 抽象模板节点类
 *
 * 继承了默认的节点方法
 */
abstract class AbsFileTemplate<T extends AbsFileTemplate<any>> extends Node<T> implements Template {
  /**
   * 模板名称
   *
   * @type {string}
   */
  name: string;

  /**
   * 模板源文件路径
   *
   * @type {string}
   */
  source?: string;

  /**
   * 模板子节点
   *
   * @type {{[p: string]: T}}
   */
  children: { [p: string]: T } = {};

  /**
   * 填充数据
   * @type {object}
   */
  data?: object;

  /**
   * 生成文件的函数
   *
   * @type {(data: object) => File}
   */
  abstract generated: (target: string) => Promise<AbsFileNode<AbsFileNode<any>>>;

  /**
   * 文件模板构造方法
   *
   * @param {string} name 模板名称
   * @param {string} source 模板文件路径
   * @param {object} data 填充数据
   * @protected
   */
  protected constructor(name: string, source?: string, data?: object) {
    super(name);
    this.name = name;
    this.source = source;
    this.data = data;
  }
}

/**
 * 文件夹对象类
 *
 */
export class DirTemplate extends AbsFileTemplate<AbsFileTemplate<any>> {

  /**
   * 构造函数
   *
   * @param {string} name 文件夹名称
   */
  private constructor(name: string) {
    super(name);
  }

  /**
   * 对外的构造函数
   *
   * @param {string} name
   * @return {DirTemplate}
   */
  public static create(name: string): DirTemplate {
    return new DirTemplate(name);
  }

  /**
   * 创建文件夹的函数
   *
   * @param {string} target
   * @return {Promise<AbsFileNode<AbsFileNode<any, undefined>, undefined>>}
   */
  generated = async (target: string): Promise<DirNode<AbsFileNode<any>>> => {
    // 组合创建文件夹的路径
    const filePath = path.join(target, this.name);

    // 创建文件夹
    await fse.ensureDir(filePath);

    // 创建生成文件的
    const fileNode: AbsFileNode<AbsFileNode<any>> = new DirNode(this.name, filePath);

    // 遍历文件夹模板的子节点，执行子文件的 generated 函数，并将生成的文件节点对象添加为文件夹节点对象的 子节点
    for (const childNode of Object.values(this.children)) {
      const childFileNode = await childNode.generated(filePath);
      fileNode.add(childFileNode);
    }

    return fileNode;
  };
}

export type DirTemplateCreator = (dirName: string) => DirTemplate

/**
 * 直接复制无需填充数据的模板文件
 */
export class FileCopyTemplate extends AbsFileTemplate<DirTemplate> {

  /**
   * 直接复制的模板对象同一个文件只需要一个，缓存住
   *
   * @type {{[p: string]: FileCopyTemplate}}
   * @private
   */
  private static cache: { [key: string]: FileCopyTemplate } = {};

  /**
   * 私有化构造方法
   *
   * @param {string} source 源文件路径
   * @param name 生成文件名/节点名
   * @private
   */
  protected constructor(source: string, name?: string) {
    if (source === '') {
      throw new Error('The template source file path is empty');
    }
    const targetName = name ?? source.split('/').pop() ?? '';
    super(targetName);
    this.name = targetName;
    this.source = source;
  }

  /**
   * 对外的构造函数
   *
   * @param {string} source 源文件
   * @param name 生成文件名/节点名
   * @return {FileCopyTemplate}
   */
  public static create(source: string, name?: string): FileCopyTemplate {
    if (!FileCopyTemplate.cache[source]) {
      FileCopyTemplate.cache[source] = new FileCopyTemplate(source, name);
    }
    return FileCopyTemplate.cache[source];
  }

  /**
   * 复制文件的函数
   *
   * @param {string} target
   * @return {Promise<AbsFileNode<AbsFileNode<any, undefined>, undefined>>}
   */
  generated = async (target: string): Promise<CopyNode<DirNode<any>>> => {
    // 获取源文件路径
    const absSourcePath = path.join(BASE_SOURCE, this.source!);

    // 文件生成路径
    const absTarget = path.join(target, this.name);

    // 复制文件
    await fse.copy(absSourcePath, absTarget);
    return new CopyNode(this.name, absTarget);
  };

  /**
   * 文件类型没有子节点
   *
   * @param {DirTemplate} child
   * @return {this}
   */
  add(child: DirTemplate): this {
    return this;
  }
}


/**
 * 通过模板+内容填充生成文件的模板类型
 */
export class FileRenderTemplate<DirTemplate> extends AbsFileTemplate<any> {

  /**
   * 模板缓存
   *
   * @type {{[p: string]: [render: HandlebarsTemplateDelegate]}}
   */
  private static TEMPLATE_FILE_CACHE: { [key: string]: HandlebarsTemplateDelegate } = {};

  /**
   * 私有化构造方法
   *
   * @param name
   * @param {string} source 源文件路径
   * @param data
   * @private
   */
  protected constructor(source: string, name?: string, data?: object) {
    if (source === '') {
      throw new Error('The template source file path is empty');
    }
    const targetName = name ?? source.split('/').pop() ?? '';
    super(targetName);
    this.name = targetName;
    this.source = source;
    this.data = data;
  }

  /**
   * 对外的构造函数
   *
   * @param {string} source 源文件
   * @param data
   * @param name
   * @return {FileCopyTemplate}
   */
  public static create<DirTemplate>(source: string, name?: string, data: object = {}): FileRenderTemplate<DirTemplate> {
    return new FileRenderTemplate<DirTemplate>(source, name, data);
  }

  /**
   * 通过模板文件路径找到对应模板文件并编译成函数
   *
   * @param {string} templatePath
   * @return {Promise<HandlebarsTemplateDelegate>}
   * @private
   */
  private static async fileCompile(templatePath: string): Promise<HandlebarsTemplateDelegate> {
    // 模板文件绝对路径
    const absPath = path.join(BASE_SOURCE, templatePath);
    if (!this.TEMPLATE_FILE_CACHE[absPath]) {
      const template = await fse.readFile(absPath, 'utf-8');

      // 注册条件判断 Helper
      handlebars.registerHelper('eq', (a, b) => {
        return a === b;
      });

      // 编译模板
      this.TEMPLATE_FILE_CACHE[absPath] = handlebars.compile(template);
    }
    return this.TEMPLATE_FILE_CACHE[absPath];
  }

  /**
   * 构造文件的函数
   *
   * @param {string} target
   * @return {Promise<AbsFileNode<AbsFileNode<any, undefined>, undefined>>}
   */
  generated = async (target: string): Promise<RenderNode<DirNode<any>>> => {
    // 将模板文件翻译为函数
    const templateDelegate = await FileRenderTemplate.fileCompile(this.source!);

    this.generated = async (target: string) => {
      // 文件生成路径
      const absTarget = path.join(target, this.name);

      // 生成的文件内容
      const text = templateDelegate(this.data);
      await fse.writeFile(absTarget, text);
      return new RenderNode(this.name, absTarget, this.data!, templateDelegate);
    };
    return this.generated(target);
  };

  /**
   * 文件类型没有子节点
   *
   * @param {DirTemplate} child
   * @return {this}
   */
  add(child: DirTemplate): this {
    return this;
  }
}


/**
 * 创建文件夹模板对象
 *
 * @param {string} name 文件夹名称
 * @return {DirTemplate}
 */
export const dirTmp = DirTemplate.create;

/**
 * 创建直接复制文件的模板对象
 *
 * @param {string} source 源文件
 * @param name 生成文件的名称/节点名称
 * @return {FileCopyTemplate}
 */
export const copyTmp = FileCopyTemplate.create;

/**
 * 创建内容填充动态生成文件的模板对象
 *
 * @param {string} source 源文件
 * @param data
 * @param name 生成文件的名称/节点名称
 * @return {FileCopyTemplate}
 */
export const renderTmp = FileRenderTemplate.create;
